
package tetris.board;

import java.awt.Color;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.Timer;
import tetris.gui.GameOver;
import tetris.gui.MusicPlayer;
import tetris.gui.SoundPlayer;
import tetris.pieces.AbstractShape;
import tetris.pieces.I_Shape;
import tetris.pieces.J_Shape;
import tetris.pieces.L_Shape;
import tetris.pieces.O_Shape;
import tetris.pieces.S_Shape;
import tetris.pieces.T_Shape;
import tetris.pieces.Z_Shape;

/**
 * A class to represent the Tetris board.
 * 
 * @author Agari NLN (agarin@uw.edu)
 * @version 1.5
 * 
 */
public class Board extends Observable
{
  /**
   * This is to represent my [], has a name for better readability.
   */
  public static final String MY_BLOCK = "[]";
  
  /**
   * This is to represent my spaces, has a name for better readability.
   */
  public static final String MY_NO_BLOCK = "  ";
  
  /**
   * The level modifier to calculate levels.
   */
  private static final int LEVEL_MODIFER = 100;
  
  /**
   * The default interval (in milliseconds) for the timer.
   */
  private static final int DEFAULT_MOVE_INTERVAL = 1000;

  /**
   * The width of the board.
   */
  private static final int COLUMNS = 10;

  /**
   * The height of the board.
   */
  private static final int ROWS = 20;
  
  /**
   * Number of lines cleared.
   */
  private static int my_lines_cleared;
  
  /**
   * The music player.
   */
  private static MusicPlayer player =  new MusicPlayer();

  /**
   * The boolean representation of the board.
   */
  private boolean[][] my_board;

  /**
   * The colors of my board.
   */
  private Color[][] my_board_colors;

  /**
   * The start position for the falling pieces.
   */
  private final Point my_position = new Point(3, -5);

  /**
   * The current falling piece.
   */
  private AbstractShape my_current_piece;

  /**
   * The next falling piece, can be previewed with later implementation.
   */
  private AbstractShape my_next_piece;

  /**
   * The timer that controls the movement of the falling shapes.
   */
  private final Timer my_timer;

  /**
   * Indicates if the board is under test.
   */
  private boolean my_is_test;

  /**
   * When board is under test, this is the index of the current piece.
   */
  private int my_test_current_piece_index;

  /**
   * When the board is under test, this is the list of shapes to feed the board.
   */
  private AbstractShape[] my_test_shapes;
  
  /**
   * Indicates if the game is over or not.
   */
  private boolean my_is_game_over;
  
  
  /**
   * 
   * An enumeration for all the possible piece that can fall down.
   * 
   * @author Agari NLN (agarin@uw.edu)
   * @version 1.5
   * 
   */
  public enum Shapes
  {
    /**
     * The I shape.
     */
    I_Shape,

    /**
     * The J shape.
     */
    J_Shape,

    /**
     * The L shape.
     */
    L_Shape,

    /**
     * The O shape.
     */
    O_Shape,

    /**
     * The S shape.
     */
    S_Shape,

    /**
     * The T shape.
     */
    T_Shape,

    /**
     * The Z shape.
     */
    Z_Shape,
  }

  /**
   * Constructing my board, the size is the number of columns by number of rows.
   */
  public Board()
  {
    this(DEFAULT_MOVE_INTERVAL);
  }

  /**
   * Constructing my board, the size is the number of columns by number of rows
   * and allows the speed of which pieces are being dropped to be set.
   * 
   * @param the_timer_interval the speed of which pieces are being dropped.
   */
  public Board(final int the_timer_interval)
  {
    super();
    my_board = new boolean[COLUMNS][ROWS];
    my_board_colors = new Color[COLUMNS][ROWS];
    my_timer = new Timer(the_timer_interval, new UpdateBoard());
    my_current_piece = makeRandomShape();
    my_next_piece = makeRandomShape();
  }

  /**
   * Constructing a board for JUnit test, instead of having the Tetris pieces
   * come out randomly it takes pieces out of an array that is fed to it.
   * 
   * @param the_shapes An array of Tetris pieces, which will be dropped in
   *          order.
   * @param the_timer_interval how fast the pieces are being dropped.
   */
  public Board(final AbstractShape[] the_shapes, final int the_timer_interval)
  {
    super();
    my_is_test = true;
    my_board = new boolean[COLUMNS][ROWS];
    my_board_colors = new Color[COLUMNS][ROWS];
    my_timer = new Timer(the_timer_interval, new UpdateBoard());
    my_test_shapes = the_shapes;
    if (the_shapes.length > 0)
    {
      my_test_current_piece_index = 0;
      my_current_piece = the_shapes[my_test_current_piece_index];
    }
    else
    {
      return;
    }

    if (the_shapes.length > 1)
    {
      my_next_piece = the_shapes[my_test_current_piece_index + 1];
    }
    else
    {
      my_next_piece = null;
    }
  }

  // representation of all filled squares
  @Override
  public String toString()
  {
    final StringBuilder string = new StringBuilder();

    for (int y = 0; y < ROWS; y++)
    {
      for (int x = 0; x < COLUMNS; x++)
      {
        if (my_board[x][y])
        {
          string.append(MY_BLOCK);
        }
        else
        {
          string.append(MY_NO_BLOCK);
        }
      }

      string.append("\n");
    }

    return string.toString();
  }

  /**
   * Starts the board timer.
   */
  public void startTimer()
  {
    if (my_is_test && my_test_shapes.length < 1)
    {
      return;
    }
    my_timer.start();
  }

  /**
   * Stops the board timer.
   */
  public void stopTimer()
  {
    my_timer.stop();
  }


  /**
   * Indicates if the board timer is running or not.
   * 
   * @return wither the board timer is running or not.
   */
  public boolean isTimerRunning()
  {
    return my_timer.isRunning();
  }

  /**
   * A class that listens for timer events, making the pieces fall when
   * appropriate.
   */
  private class UpdateBoard implements ActionListener
  {
    @Override
    public void actionPerformed(final ActionEvent the_event)
    { 
      if (!my_is_test)
      {
        final File[] files = new File[100000];
        for (int i = 0; i < files.length; i++)
        {
          files[i] = new File("Korobeiniki.mid");
        }
        if (!player.isStarted())
        {
          player.newList(files);
        }
      }
      
      if (my_is_game_over || (my_is_test && my_current_piece == null))
      {
        my_timer.stop();
        new GameOver(new JFrame(), "Game End", "Game Over");
        return;
      }

      if (canMoveDown())
      {
        moveDown();
      }
      else
      {
        engrainShape();
        addPiece();
      }
    }
  }

  /**
   * Method to create a random piece to fall from the "sky" and also creating
   * other piece for previewing.
   */
  public void addPiece()
  {
    my_current_piece = my_next_piece;

    if (my_is_test)
    {
      my_test_current_piece_index++;

      if (my_test_current_piece_index + 1 < my_test_shapes.length)
      {
        my_next_piece = my_test_shapes[my_test_current_piece_index + 1];
      }
      else
      {
        // This is here because I have a null check later.
        my_next_piece = null;
      }
    }
    else
    {
      my_next_piece = makeRandomShape();
    }
  }

  /**
   * Clear out the row that is filled with pieces.
   */
  public void clearFullRows()
  {
    for (int y = 0; y < ROWS; y++)
    {
      if (isRowFull(y))
      {
        clearRow(y);
        my_lines_cleared++;
        // This is a basic way to increase the speed of the pieces dropping.
        if (!my_is_test)
        {
          my_timer.setDelay(DEFAULT_MOVE_INTERVAL - (getLinesCleared() * 5));
        }
        shiftRowsDown(y);
      }
    }
  }
  
  

  /**
   * Check if the row is filled with pieces.
   * 
   * @param the_row_index the index to represent what row is being checked.
   * @return wither or not the row is filled.
   */
  private boolean isRowFull(final int the_row_index)
  {
    boolean flag = true;
    for (int x = 0; x < COLUMNS; x++)
    {
      if (!my_board[x][the_row_index])
      {
        flag = false;
      }
    }
    return flag;
  }

  /**
   * Method to clear out the filled row.
   * 
   * @param the_row_index index the index to represent what row is being
   *          cleared.
   */
  private void clearRow(final int the_row_index)
  {
    for (int x = 0; x < COLUMNS; x++)
    {
      my_board[x][the_row_index] = false;
      this.setChanged();
      final SoundPlayer player = new SoundPlayer();
      player.play("tetris/audio/bloop.wav");
    }
    this.notifyObservers();
  }

  /**
   * Method to pull the rows above down and also clear the very top row.
   * 
   * @param the_start_index the starting row of which all rows above are getting
   *          pulled down.
   */
  private void shiftRowsDown(final int the_start_index)
  {
    if (the_start_index > 0)
    {
      int current_row = the_start_index;

      while (current_row > 0)
      {
        for (int x = 0; x < COLUMNS; x++)
        {
          my_board[x][current_row] = my_board[x][current_row - 1];
          my_board_colors[x][current_row] = my_board_colors[x][current_row - 1];
        }
        current_row--;
        
      }
    }
    // clear the top row always
    clearRow(0);
  }

  /**
   * Move the piece to the left.
   */
  public void moveLeft()
  {
    if (canMoveLeft())
    {
      my_current_piece.moveLeft();
      this.setChanged();
    }
    this.notifyObservers();
  }

  /**
   * Move the piece to the right.
   */
  public void moveRight()
  {
    if (canMoveRight())
    {
      my_current_piece.moveRight();
      this.setChanged();
    }
    this.notifyObservers();
  }

  /**
   * Move the piece down by one block.
   */
  public void moveDown()
  {
    if (canMoveDown())
    {
      my_current_piece.moveDown();
      this.setChanged();
    }
    this.notifyObservers();
  }

  /**
   * Method to determine wither a piece can move to the right.
   * 
   * @return wither a piece can move to the right.
   */
  public boolean canMoveRight()
  {
    final Point[] block_positions = my_current_piece.blockPositions();
    boolean flag = true;

    // check if the current piece is colliding with another
    for (int i = 0; i < block_positions.length; i++)
    {
      final int x = block_positions[i].x + 1;
      final int y = block_positions[i].y;
      if (y >= 0 && (x >= COLUMNS || my_board[x][y]))
      {
        flag = false;
      }
    }
    return flag;
  }

  /**
   * Method to determine wither a piece can move to the left.
   * 
   * @return wither a piece can move to the left.
   */
  public boolean canMoveLeft()
  {
    final Point[] block_positions = my_current_piece.blockPositions();
    boolean flag = true;

    // check if the current piece is colliding with another
    for (int i = 0; i < block_positions.length; i++)
    {
      final int x = block_positions[i].x - 1;
      final int y = block_positions[i].y;

      if (y >= 0 && (x < 0 || my_board[x][y]))
      {
        flag = false;
      }
    }
    return flag;
  }

  /**
   * Rotate the piece to the left.
   */
  public void rotateLeft()
  {
    if (canRotateLeft())
    {
      my_current_piece.rotateLeft();
      this.setChanged();
      final SoundPlayer player = new SoundPlayer();
      player.play("tetris/audio/chime.wav");
    }
    this.notifyObservers();
  }

  /**
   * Rotate the piece to the right.
   */
  public void rotateRight()
  {
    if (canRotateRight())
    {
      my_current_piece.rotateRight();
      this.setChanged();
    }
    this.notifyObservers();
  }

  /**
   * Check if the piece can rotate right.
   * 
   * @return wither if the piece can rotate right.
   */
  public boolean canRotateRight()
  {
    my_current_piece.rotateRight();
    final boolean success = checkBoundaryAndCollision();
    my_current_piece.rotateLeft();
    return success;
  }

  /**
   * Check if the piece can rotate left.
   * 
   * @return wither if the piece can rotate left
   */
  public boolean canRotateLeft()
  {
    my_current_piece.rotateLeft();
    final boolean success = checkBoundaryAndCollision();
    my_current_piece.rotateRight();
    return success;
  }

  /**
   * Check if the piece can move down.
   * 
   * @return wither the piece can move town.
   */
  private boolean canMoveDown()
  {
    final Point[] block_positions = my_current_piece.blockPositions();
    boolean flag = true;

    // check if the current piece is colliding with another
    for (int i = 0; i < block_positions.length; i++)
    {
      final int x = block_positions[i].x;
      final int y = block_positions[i].y + 1;

      if (y >= 0 && (y >= ROWS || my_board[x][y]))
      {
        flag = false;
        break;
      }
    }

    return flag;
  }

  /**
   * Method to make the piece into part of the board when the piece cannot move
   * anymore.
   */
  private void engrainShape()
  {
    final Point[] blockPositions = my_current_piece.blockPositions();

    for (int i = 0; i < blockPositions.length; i++)
    {
      if (blockPositions[i].y < 0)
      {
        my_is_game_over = true;
      }
      else
      {
        final SoundPlayer player = new SoundPlayer();
        player.play("tetris/audio/click.wav");
        
        my_board[blockPositions[i].x][blockPositions[i].y] = true;
        my_board_colors[blockPositions[i].x][blockPositions[i].y] = 
          my_current_piece.getColor(); 
      }
    }

    clearFullRows();
  }

  /**
   * Method to randomly create a shape choosing from the 7 possible Tetris
   * pieces.
   * 
   * @return One randomly chosen Tetris piece.
   */
  private AbstractShape makeRandomShape()
  {
    final Random random_select = new Random();
    final Shapes[] shapes = Shapes.values();

    final Point position = (Point) my_position.clone();
    AbstractShape created_random_piece;

    switch (shapes[random_select.nextInt(shapes.length)])
    {
      case I_Shape:
        created_random_piece = new I_Shape(position);
        break;
      case J_Shape:
        created_random_piece = new J_Shape(position);
        break;
      case L_Shape:
        created_random_piece = new L_Shape(position);
        break;
      case O_Shape:
        created_random_piece = new O_Shape(position);
        break;
      case S_Shape:
        created_random_piece = new S_Shape(position);
        break;
      case T_Shape:
        created_random_piece = new T_Shape(position);
        break;
      case Z_Shape:
        created_random_piece = new Z_Shape(position);
        break;
      default:
        created_random_piece = new Z_Shape(position);
        break;
    }
    return created_random_piece;
  }

  /**
   * Method to check for boundary and collision.
   * 
   * @return wither if hit the boundary or collide into the board(other pieces)
   */
  private boolean checkBoundaryAndCollision()
  {
    boolean success = true;

    if (my_current_piece.myPosition().y >= 0)
    {
      final Point[] block_positions = my_current_piece.blockPositions();

      for (int i = 0; i < block_positions.length; i++)
      {
        final int x = block_positions[i].x;
        final int y = block_positions[i].y;

        // boundary checks + collision check
        if (x < 0 || x >= COLUMNS || y >= ROWS || my_board[x][y])
        {
          success = false;
          break;
        }
      }
    }
    else
    {
      // the piece is still above the top of the board
      success = false;
    }

    return success;
  }

  /**
   * Representing all the filled spots on the board in an array of points
   * contains the coordinates of filled spots.
   * 
   * @return an array of points contains the coordinates of filled spots.
   */
  public ArrayList<Point> pointsOnBoard()
  {
    final ArrayList<Point> points = new ArrayList<Point>();

    for (int x = 0; x < COLUMNS; x++)
    {
      for (int y = 0; y < ROWS; y++)
      {
        if (my_board[x][y])
        {
          points.add(new Point(x, y));
        }
      }
    }
    return points;
  }

  /**
   * Method to get the preview piece.
   * 
   * @return the preview piece.
   */
  public AbstractShape getNextPiece()
  {
    return my_next_piece;
  }

  /**
   * Method to get the current falling piece.
   * 
   * @return the current falling piece.
   */
  public AbstractShape getCurrentPiece()
  {
    return my_current_piece;
  }

  /**
   * Determine whether if the game is over.
   * @return whether the game is over.
   */
  public boolean isGameOver()
  {
    return my_is_game_over;
  }
  
  /**
   * Method to drop the current Tetris piece instantly to the board.
   */
  public void instantDrop()
  {
    while (this.canMoveDown())
    {
      this.moveDown();
    }
  }

  /**
   * Gets the color of the board at the specified coordinate.
   * 
   * @param the_x X position on the board
   * @param the_y Y position on the board
   * @return The color of the board at the (x,y) coordinate given
   */
  public Color getBoardColor(final int the_x, final int the_y)
  {
    return my_board_colors[the_x][the_y];
  }
  
  /**
   * Get the number of lines cleared by the player.
   * @return the number of lines cleared.
   */
  public static int getLinesCleared()
  {
    return my_lines_cleared;
  }
  
  /**
   * Get the current level of the game.
   * @return the current level of the game.
   */
  public int getLevel()
  {
    //every 100 lines cleared the level will increase by one
    return (DEFAULT_MOVE_INTERVAL - my_timer.getDelay()) / LEVEL_MODIFER + 1;
  }
  
}
